﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading;
using System.Web;
using log4net;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Collections;


namespace Sherwood.SignOn.Client.UrlFilter
{
    /// <summary>
    /// Implements a HttpModule which checks the user's session and redirects the user
    /// to login (sso server) if no session exists and the requested URL requires authentication.
    /// </summary>
    public class UrlFilterModule : IHttpModule
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof(UrlFilterModule));

        public SignOnClient SsoClient { get; set; }

        public const string VARIABLE_PREFIX = "SIGNON_";


        #region IHttpModule Members

        public void Dispose()
        {

        }

        public void Init(HttpApplication context)
        {
            try
            {
                SsoClient = new SignOnClient();

                if (SsoClient.Settings != null)
                {
                    context.AuthenticateRequest += context_AuthenticateRequest;
                    context.PostAcquireRequestState += context_PostAcquireRequestState;
                }
                else
                {
                    //If settings have not been loaded, then we assume that the single sign on client shouldn't be run for this website.
                    Log.Debug("Settings file could not be loaded. Skipping filter.");
                    return;
                }
            }
            catch (Exception ex)
            {
                Log.Error("Exception occured trying to apply url filter", ex);
                throw new HttpException(403, "Failed to authenticate request. See log for more details.");
            }
        }

        void context_AuthenticateRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;
            try
            {
                application.Context.Items[VARIABLE_PREFIX + "CLIENT"] = SsoClient;
                ApplyUrlFilter(application);
            }
            catch (ThreadAbortException)
            {
                throw;
                // thrown when redirecting with endResponse = true
            }
            catch (Exception ex)
            {
               Log.Error("Exception occured trying to apply url filter", ex);
                throw new HttpException(403, "Failed to authenticate request. See log for more details.");
            }
        }
        
        void context_PostAcquireRequestState(object sender, EventArgs e)
        {
            try
            {
                //At this point we can set ASP.Net session variables, which will be available in ASP.Net applications.
                SetSignOnVariables((HttpApplication)sender);
            }
            catch (Exception ex)
            {
                Log.Error("Exception occured trying to set sign on variables", ex);
            }
        }

        /// <summary>
        /// Checks all rules in defined for the URL filter and redirects user if access has been denied. 
        /// </summary>
        /// <param name="application"></param>
        protected virtual void ApplyUrlFilter(HttpApplication application)
        {
            HttpContext context = application.Context;

            var filterEngine = new UrlFilterEngine();
            var hasRequest = context.Request != null;
            string host = hasRequest && context.Request.UserHostAddress != null ? context.Request.UserHostAddress : "";
            string referrer = hasRequest && context.Request.UrlReferrer != null
                                  ? context.Request.UrlReferrer.ToString()
                                  : string.Empty;
            string url = hasRequest && context.Request.Url != null && context.Request.Url.AbsoluteUri != null ? context.Request.Url.AbsoluteUri : "";
            Log.Debug("Raw url: " + url);
            url = GetOriginalUrl(url);
            Log.Debug("User url: " + url);
            string redirectUrl;
            bool requireNonPersistentSession;
            bool userHasAccess;

            //If current url is the sign in url, then peform sign in
            if (IsSignInUrl(url))
            {
                SignIn(context);
                return;
            }

            //If current url is the sign out url, then perform sign out
            if (IsSignOutUrl(url))
            {
                SignOut(context);
                return;
            }

            //If user isn't signed in yet
            if (!SsoClient.IsUserSignedIn)
            {
                Log.Debug("User not signed in. Checking annonymous access for url: " + url);
                //Check if anonymous user has access
                bool requireSession;
                
                userHasAccess = filterEngine.CheckAccess(url, "", host, new string[] { }, referrer, out redirectUrl, out requireSession, out requireNonPersistentSession);

                if (!userHasAccess)
                {
                    Log.Debug("Url doesn't have annonymous access. Redirecting to login. URL: " + url);
                    //If anonymous access is blocked, redirect user to login page and end response.
                    //Note that redirectUrls for unauthenticated users are ignored.
                    //Once the user logs in and returns to the same page, the redirect url will be 
                    //taken into consideration.
                    SsoClient.RedirectToSsoServer(true, requireNonPersistentSession);
                    context.Response.End();
                }
                else
                {
                    if (requireSession && !(hasRequest && context.Request.Browser.Crawler))
                    {
                        //A session is required for this url, even though anonymous authentication is allowed.
                        //Redirect user to sign on server to get a ticket, but do not require login 
                        //This means if the user is not already authenticated, an annonymous session will
                        //be created by the server.
                        //Crawlers will never sign in, so we do not redirect them to the server to get a session.
                        Log.Debug("Redirecting user for annonymous sign on to: " + url);
                        SsoClient.RedirectToSsoServer(false, requireNonPersistentSession);
                        context.Response.End();
                    }
                    else
                    {
                        //No session is require for this URL so just redirect user if redirect url has been provided
                        //Otherwise continue processing the URL as normal (authentication passed...)
                        if (!string.IsNullOrEmpty(redirectUrl))
                        {
                            context.Response.Redirect(redirectUrl);
                        }
                    }
                }
            }
            else
            {
                string username = SsoClient.Username != null ? SsoClient.Username.Trim() : "";
                string[] roles = SsoClient.UserRoles != null ? SsoClient.UserRoles.ToArray() : new string[] { };

                var rolesString = "";
                foreach (var r in roles)
                    rolesString += r + ", ";
                rolesString = rolesString.TrimEnd(',');

                Log.Debug(username + " logged in. Checking access for roles: " + rolesString);
                //Check if user has access to the current url
                userHasAccess = filterEngine.CheckAccess(url, username, host, roles, referrer, out redirectUrl, out requireNonPersistentSession);

                if (userHasAccess)
                    Log.Debug(username + " has access to url: " + url);
                else
                    Log.Debug(username + " does not have access to url: " + url);

                //Check if current session is persistent and if current url requires a non-persistent session.
                if (requireNonPersistentSession && SsoClient.IsPersistentSession)
                {
                    SsoClient.RedirectToSsoServer(true, true);
                    context.Response.End();
                }
                else
                {                    
                    //Redirect user if url filter engine provides a redirect
                    if (!string.IsNullOrEmpty(redirectUrl))
                    {
                        context.Response.Redirect(redirectUrl);
                    }
                    else
                    {
                        //Send an empty response if url filter engine hasn't provided a redirect and user does not have access.
                        if (!userHasAccess)
                        {
                            HttpContext.Current.Response.StatusCode = 403;
                            context.Response.End();
                        }

                    }
                }
            }
        }


        /// <summary>
        /// Sign in user locally
        /// </summary>
        protected void SignIn(HttpContext context)
        {
            try
            {
                Log.Debug("Attempting to sign in user locally.");
                string ticket = context.Request.QueryString["ticket"] ?? "";
                string signature = context.Request.QueryString["signature"] ?? "";
                string returnurl = context.Request.QueryString["returnurl"] ?? "";

                if (SsoClient.ClientSignOn(ticket, signature))
                {
                    if (!string.IsNullOrEmpty(returnurl))
                    {
                        Log.Debug("User successfully signed in locally. Redirecting to " + returnurl);
                        context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                        context.Response.Redirect(returnurl, true);
                    }
                    else
                    {
                        Log.Debug("User could not be signed in locally. No redirect url provided.");
                        throw new HttpException(500, "Failed to sign on");
                    }
                }
                else
                {
                    Log.Debug("User could not be signed in locally! Redirecting to sign on server.");
                    SsoClient.RedirectToSsoServer(returnurl, true);
                }
            }
            catch (ThreadAbortException) { }
            catch (Exception ex)
            {
                Log.Error("Exception occured trying to sign in client.", ex);
            }
        }


        /// <summary>
        /// Sign out user locally
        /// </summary>
        protected void SignOut(HttpContext context)
        {
            try
            {
                Log.Debug("Attempting to sign user out.");
                string ticket = context.Request.QueryString["ticket"] ?? "";
                string signature = context.Request.QueryString["signature"] ?? "";

                SsoClient.ClientSignOut(ticket, signature);

                context.Response.StatusCode = 204; //204: no content
                context.Response.End();

                
            }
            catch (ThreadAbortException) { }
            catch (Exception ex)
            {
                Log.Error("Exception occured trying to sign out client.", ex);
            }
        }

        public static SignOnClient CurrentSignOnClient
        {
            get
            {
                return HttpContext.Current != null ? HttpContext.Current.Items[VARIABLE_PREFIX + "CLIENT"] as SignOnClient : null;
            }
        }


        /// <summary>
        /// Sets variables that can be read by server side scripts (ASP.Net / PHP).
        /// Note that ASP.Net can read session variables, but PHP is limited to reading request headers because
        /// it does not use the .Net framework for session management.
        /// </summary>
        /// <param name="appliction"></param>
        protected virtual void SetSignOnVariables(HttpApplication application)
        {

            HttpContext context = application.Context;

            //Set session variables for the convencience of ASP.Net web applications
            if (SsoClient.IsUserSignedIn)
            {
                string username = SsoClient.Username ?? "";
                var userProfile = SsoClient.UserProfile;
                string firstName = userProfile != null && userProfile.firstName != null ? userProfile.firstName : "";
                string lastName = userProfile != null && userProfile.lastName != null ? userProfile.lastName : "";
                int gender = userProfile != null ? (int)(userProfile.gender ?? 0) : 0;
                var roleList = new List<string>();
                var cultureList = new List<string>();
                var attributeList = new NameValueCollection();
                if (userProfile != null)
                {
                    if (userProfile.role != null)
                    {
                        foreach (var role in userProfile.role)
                        {
                            string strRole = role.source + ":" + role.name;
                            if (!roleList.Contains(strRole))
                                roleList.Add(strRole);
                        }
                    }

                    if (userProfile.attribute != null)
                    {
                        foreach (var attribute in userProfile.attribute)
                        {
                            string strName = attribute.source + ":" + attribute.name;
                            if (!attributeList.AllKeys.Contains(strName))
                                attributeList.Add(strName, attribute.value ?? "");
                        }
                    }

                    if (userProfile.culture != null && userProfile.culture.Count > 0)
                    {
                        List<culture> sortedCultureList = userProfile.culture.ToList();
                        sortedCultureList.Sort(
                            (a, b) =>
                            a.priority.GetValueOrDefault(int.MaxValue).CompareTo(
                                b.priority.GetValueOrDefault(int.MaxValue)));

                        foreach (var culture in sortedCultureList)
                        {
                            if (!cultureList.Contains(culture.name))
                                cultureList.Add(culture.name);
                        }
                    }
                }


                var variables = new NameValueCollection();
                if (roleList.Count > 0)
                {
                    variables[VARIABLE_PREFIX + "Roles"] = roleList.ToArray().Aggregate((current, next) => current + "," + next);
                }
                else
                {
                    variables[VARIABLE_PREFIX + "Roles"] = "";
                }
                foreach (string attributeName in attributeList.Keys)
                {
                    variables[VARIABLE_PREFIX + "Attribute_" + attributeName] = attributeList[attributeName] ?? "";
                }
                if (cultureList.Count > 0)
                {
                    variables[VARIABLE_PREFIX + "Cultures"] = cultureList.ToArray().Aggregate((current, next) => current + "," + next);
                }
                else
                {
                    variables[VARIABLE_PREFIX + "Cultures"] = "";
                }
                variables[VARIABLE_PREFIX + "SessionId"] = SsoClient.SessionId;
                variables[VARIABLE_PREFIX + "UserAccountId"] = SsoClient.UserAccountId;
                variables[VARIABLE_PREFIX + "SignOnSessionId"] = SsoClient.SignOnSessionId;
                variables[VARIABLE_PREFIX + "Username"] = username;
                variables[VARIABLE_PREFIX + "FirstName"] = firstName;
                variables[VARIABLE_PREFIX + "LastName"] = lastName;
                variables[VARIABLE_PREFIX + "Gender"] = gender.ToString();

                if (context != null && context.Request != null)
                {
                    /* try
                     {
                         foreach (string variableName in variables.Keys)
                         {
                             context.Request.ServerVariables[variableName] = variables[variableName];
                         }
                     }
                     catch (PlatformNotSupportedException) //In some cases, server variables can't be set (application trust level?)
                     { }*/

                    if (context.Session != null && !context.Session.IsReadOnly)
                    {
                        //clear existing values (esp. important to clear attributes)
                        List<string> keysToRemove = new List<string>();
                        foreach (string sessionKey in context.Session.Keys)
                        {
                            if (sessionKey.StartsWith(VARIABLE_PREFIX))
                                keysToRemove.Add(sessionKey);
                        }
                        foreach (string sessionKey in keysToRemove)
                            context.Session.Remove(sessionKey);

                        //set new values
                        foreach (string variableName in variables.Keys)
                        {
                            context.Session[variableName] = variables[variableName];
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Parses the passed in rawUrl so that redirects to error pages (e.g. 404 not found) are removed 
        /// and the original url (e.g. one that resembles what was typed into the browser) is returned.
        /// </summary>
        /// <param name="rawUrl"></param>
        /// <returns></returns>
        public static string GetOriginalUrl(string rawUrl)
        {
            Regex re = new Regex("[^\\?]+\\?[0-9]+;(.+)");
            Match match = re.Match(rawUrl);
            if (match != null && match.Success)
            {
                return match.Groups[1].Value.Replace(":80/", "/");
            }
            return rawUrl;
        }

        /// <summary>
        /// Determines whether the current url is the sign in url
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        protected virtual bool IsSignInUrl(string url)
        {
            if (Log.IsDebugEnabled)
            {
                Log.Debug("Checking if signin url. Comparing '" + GetUrlPathWithoutHost(SsoClient.Settings.client.signOutUrl) + "' to '" + GetUrlPathWithoutHost(url) + "'");
            }
            return GetUrlPathWithoutHost(SsoClient.Settings.client.signInUrl).Equals(GetUrlPathWithoutHost(url), StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Determines whether the current url is the sign in url
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        protected virtual bool IsSignOutUrl(string url)
        {
            if (Log.IsDebugEnabled)
            {
                Log.Debug("Checking if signout url. Comparing '" + GetUrlPathWithoutHost(SsoClient.Settings.client.signOutUrl) + "' to '" + GetUrlPathWithoutHost(url) + "'");
            }
            return GetUrlPathWithoutHost(SsoClient.Settings.client.signOutUrl).Equals(GetUrlPathWithoutHost(url), StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Strips the query string, host and protocol off a url.
        /// This allows for a looser match when determining urls to bypass.
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        protected virtual string GetUrlPathWithoutHost(string url)
        {
            //Remove query string
            url = url.ToLower().Trim().Split('?')[0];

            if (url.StartsWith("~") && HttpContext.Current != null)
                return (HttpContext.Current.Request.ApplicationPath + url.Substring(1)).TrimStart('/');

            //Split url (and combine protocol part with host)
            string[] parts = url.Replace("://", "").Split('/');

            //Rebuild url without host or protocol
            string urlWithoutHost = "";
            for (int i = 0; i < parts.Length; i++)
            {
                if (i == 0)
                {
                    string part = parts[i];
                    if (part.Contains("http") || part.Contains(".") || (part.Contains("[") && part.Contains("]")))
                        continue;
                }
                urlWithoutHost += "/" + parts[i];
            }
            return urlWithoutHost.TrimStart('/');
        }


        #endregion
    }
}
